package com.spotalog.server.service;

import static com.googlecode.objectify.ObjectifyService.ofy;

import java.util.Date;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Provider;

import lombok.extern.slf4j.Slf4j;

import com.google.appengine.api.search.AddException;
import com.google.appengine.api.search.Cursor;
import com.google.appengine.api.search.Document;
import com.google.appengine.api.search.Index;
import com.google.appengine.api.search.ListRequest;
import com.google.appengine.api.search.Query;
import com.google.appengine.api.search.QueryOptions;
import com.google.appengine.api.search.SortExpression;
import com.google.appengine.api.search.SortOptions;
import com.google.appengine.api.search.StatusCode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result;
import com.googlecode.objectify.TxnType;
import com.spotalog.server.annotations.Transact;
import com.spotalog.shared.exceptions.NotFoundException;
import com.spotalog.shared.model.DatastoreObject;

@Slf4j
public abstract class AbstractService<E extends DatastoreObject> implements
		Service<E> {
	private final Class<E> clazz;
	private Provider<Date> now;
	private Function<E, Document> toDocument;
	private Provider<ListRequest.Builder> listRequests;
	private Cursor.Builder cursorBuilder;
	private Index searchIndex;
	private Splitter splitter;
	private Joiner joiner;
	protected Provider<SortOptions.Builder> sortOptions;
	protected Provider<SortExpression.Builder> sortExpressions;
	protected Provider<QueryOptions.Builder> queryOptions;
	protected Provider<Query.Builder> queryBuilers;

	protected AbstractService(Class<E> clazz) {
		this.clazz = clazz;
	}

	protected final Index getIndex() {
		return this.searchIndex;
	}

	@Override
	public Optional<E> read(long id) {
		return Optional
				.fromNullable(ofy().load().type(this.clazz).id(id).get());
	}

	@Override
	public E readSafe(long id) throws NotFoundException {
		Optional<E> read = read(id);
		if (!read.isPresent()) {
			throw new NotFoundException("%s: %d does NOT exist!",
					this.clazz.getSimpleName(), id);
		}
		return read.get();
	}

	@Override
	@Transact(TxnType.REQUIRED)
	public E upsert(E entity) {
		if (entity != null) {
			if (entity.getDateCreated() == null) {
				entity.setDateCreated(this.now.get());
			}
			Result<Key<E>> result = ofy().save().entity(entity);
			if (entity.getId() == null) {
				result.now();
			}
			removeFromIndex(entity.getId());
			addToIndex(entity);
		}
		return entity;
	}

	@Override
	@Transact(TxnType.REQUIRED)
	public void delete(long id) {
		ofy().delete().type(this.clazz).id(id);
		removeFromIndex(id);
	}

	@VisibleForTesting
	protected Cursor buildCursor(String cursorString) {
		try {
			return this.cursorBuilder.build(cursorString);
		} catch (Exception ex) {
			log.warn("Failed to reconstruct Cursor from String: {}",
					cursorString);
			return null;
		}
	}

	@VisibleForTesting
	protected final String generateKey(long id) {
		return this.joiner.join(this.clazz.getName(), id);
	}

	@VisibleForTesting
	protected final long getKey(String key) {
		FluentIterable<String> split = FluentIterable.from(this.splitter
				.split(key));
		return Long.valueOf(split.get(1));
	}

	protected void addToIndex(E entity) {
		Document document = this.toDocument.apply(entity);
		try {
			this.searchIndex.add(document);
		} catch (AddException ex) {
			log.error("Failed to add documents: {}", ex);
			if (StatusCode.TRANSIENT_ERROR.equals(ex.getOperationResult()
					.getCode())) {
				// TODO retry adding document
			}
		}
	}

	protected void removeFromIndex(long id) {
		List<String> keys = Lists.newArrayList();
		try {
			ListRequest idRequest = this.listRequests.get()
					.setReturningIdsOnly(true).build();
			for (Document document : this.searchIndex.listDocuments(idRequest)) {
				if (document.getId().equals(generateKey(id))) {
					keys.add(document.getId());
				}
			}
			this.searchIndex.removeAsync(keys);
		} catch (RuntimeException ex) {
			log.error("Failed to remove documents: {}", ex);
		}
	}

	@Inject
	void setCursorBuilder(Cursor.Builder cursorBuilder) {
		this.cursorBuilder = cursorBuilder;
	}

	@Inject
	void setSplitter(Splitter splitter) {
		this.splitter = splitter;
	}

	@Inject
	void setJoiner(Joiner joiner) {
		this.joiner = joiner;
	}

	@Inject
	void setSearchIndex(Index searchIndex) {
		this.searchIndex = searchIndex;
	}

	@Inject
	void setListRequestBuilders(Provider<ListRequest.Builder> listRequests) {
		this.listRequests = listRequests;
	}

	@Inject
	void setDocumentCreator(Function<E, Document> toDocument) {
		this.toDocument = toDocument;
	}

	@Inject
	void setNowProvider(Provider<Date> now) {
		this.now = now;
	}

	@Inject
	void setSortOptionss(Provider<SortOptions.Builder> sortOptions) {
		this.sortOptions = sortOptions;
	}

	@Inject
	void setSortExpressions(Provider<SortExpression.Builder> sortExpressions) {
		this.sortExpressions = sortExpressions;
	}

	@Inject
	void setQueryOptions(Provider<QueryOptions.Builder> queryOptions) {
		this.queryOptions = queryOptions;
	}

	@Inject
	void setQueryBuilers(Provider<Query.Builder> queryBuilers) {
		this.queryBuilers = queryBuilers;
	}
}
